package org.andengine.engine;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Vibrator;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;

import org.andengine.BuildConfig;
import org.andengine.audio.music.MusicFactory;
import org.andengine.audio.music.MusicManager;
import org.andengine.audio.sound.SoundFactory;
import org.andengine.audio.sound.SoundManager;
import org.andengine.engine.camera.Camera;
import org.andengine.engine.handler.DrawHandlerList;
import org.andengine.engine.handler.IDrawHandler;
import org.andengine.engine.handler.IUpdateHandler;
import org.andengine.engine.handler.UpdateHandlerList;
import org.andengine.engine.handler.runnable.RunnableHandler;
import org.andengine.engine.options.EngineOptions;
import org.andengine.entity.scene.Scene;
import org.andengine.input.sensor.SensorDelay;
import org.andengine.input.sensor.acceleration.AccelerationData;
import org.andengine.input.sensor.acceleration.AccelerationSensorOptions;
import org.andengine.input.sensor.acceleration.IAccelerationListener;
import org.andengine.input.sensor.location.ILocationListener;
import org.andengine.input.sensor.location.LocationProviderStatus;
import org.andengine.input.sensor.location.LocationSensorOptions;
import org.andengine.input.sensor.orientation.IOrientationListener;
import org.andengine.input.sensor.orientation.OrientationData;
import org.andengine.input.sensor.orientation.OrientationSensorOptions;
import org.andengine.input.touch.TouchEvent;
import org.andengine.input.touch.controller.ITouchController;
import org.andengine.input.touch.controller.ITouchEventCallback;
import org.andengine.input.touch.controller.MultiTouchController;
import org.andengine.input.touch.controller.SingleTouchController;
import org.andengine.opengl.font.FontFactory;
import org.andengine.opengl.font.FontManager;
import org.andengine.opengl.shader.ShaderProgramManager;
import org.andengine.opengl.texture.TextureManager;
import org.andengine.opengl.texture.atlas.bitmap.BitmapTextureAtlasTextureRegionFactory;
import org.andengine.opengl.util.GLState;
import org.andengine.opengl.vbo.VertexBufferObjectManager;
import org.andengine.util.debug.Debug;
import org.andengine.util.time.TimeConstants;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 12:21:31 - 08.03.2010
 */
public class Engine implements SensorEventListener, OnTouchListener, ITouchEventCallback, LocationListener {
    // ===========================================================
    // Constants
    // ===========================================================

    private static final SensorDelay SENSORDELAY_DEFAULT = SensorDelay.GAME;
    private static final int UPDATEHANDLERS_CAPACITY_DEFAULT = 8;
    private final UpdateHandlerList mUpdateHandlers = new UpdateHandlerList(Engine.UPDATEHANDLERS_CAPACITY_DEFAULT);

    // ===========================================================
    // Fields
    // ===========================================================
    private static final int DRAWHANDLERS_CAPACITY_DEFAULT = 4;
    private final DrawHandlerList mDrawHandlers = new DrawHandlerList(Engine.DRAWHANDLERS_CAPACITY_DEFAULT);
    protected final Camera mCamera;
    private final EngineLock mEngineLock;
    private final UpdateThread mUpdateThread;
    private final RunnableHandler mUpdateThreadRunnableHandler = new RunnableHandler();
    private final EngineOptions mEngineOptions;
    private final VertexBufferObjectManager mVertexBufferObjectManager = new VertexBufferObjectManager();
    private final TextureManager mTextureManager = new TextureManager();
    private final FontManager mFontManager = new FontManager();
    private final ShaderProgramManager mShaderProgramManager = new ShaderProgramManager();
    private final SoundManager mSoundManager;
    private final MusicManager mMusicManager;
    protected Scene mScene;
    protected int mSurfaceWidth = 1; // 1 to prevent accidental DIV/0
    protected int mSurfaceHeight = 1; // 1 to prevent accidental DIV/0
    private boolean mRunning;
    private boolean mDestroyed;
    private long mLastTick;
    private float mSecondsElapsedTotal;
    private ITouchController mTouchController;
    private Vibrator mVibrator;
    private Display mDefaultDisplay;
    private ILocationListener mLocationListener;
    private Location mLocation;
    private IAccelerationListener mAccelerationListener;
    private AccelerationData mAccelerationData;
    private IOrientationListener mOrientationListener;
    private OrientationData mOrientationData;

    // ===========================================================
    // Constructors
    // ===========================================================

    public Engine(final EngineOptions pEngineOptions) {
        /* Initialize Factory and Manager classes. */
        BitmapTextureAtlasTextureRegionFactory.reset();
        SoundFactory.onCreate();
        MusicFactory.onCreate();
        FontFactory.onCreate();
        this.mVertexBufferObjectManager.onCreate();
        this.mTextureManager.onCreate();
        this.mFontManager.onCreate();
        this.mShaderProgramManager.onCreate();

		/* Apply EngineOptions. */
        this.mEngineOptions = pEngineOptions;
        if (this.mEngineOptions.hasEngineLock()) {
            this.mEngineLock = pEngineOptions.getEngineLock();
        } else {
            this.mEngineLock = new EngineLock(false);
        }
        this.mCamera = pEngineOptions.getCamera();

		/* Touch. */
        if (this.mEngineOptions.getTouchOptions().needsMultiTouch()) {
            this.setTouchController(new MultiTouchController());
        } else {
            this.setTouchController(new SingleTouchController());
        }

		/* Audio. */
        if (this.mEngineOptions.getAudioOptions().needsSound()) {
            this.mSoundManager = new SoundManager(this.mEngineOptions.getAudioOptions().getSoundOptions().getMaxSimultaneousStreams());
        } else {
            this.mSoundManager = null;
        }
        if (this.mEngineOptions.getAudioOptions().needsMusic()) {
            this.mMusicManager = new MusicManager();
        } else {
            this.mMusicManager = null;
        }

		/* Start the UpdateThread. */
        if (this.mEngineOptions.hasUpdateThread()) {
            this.mUpdateThread = this.mEngineOptions.getUpdateThread();
        } else {
            this.mUpdateThread = new UpdateThread();
        }
        this.mUpdateThread.setEngine(this);
    }

    public void startUpdateThread() throws IllegalThreadStateException {
        this.mUpdateThread.start();
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    public synchronized boolean isRunning() {
        return this.mRunning;
    }

    public synchronized void start() {
        if (!this.mRunning) {
            this.mLastTick = System.nanoTime();
            if (this.mSoundManager != null) {
                this.mSoundManager.onResume();
            }
            this.mRunning = true;
        }
    }

    public synchronized void stop() {
        if (this.mRunning) {
            if (this.mSoundManager != null) {
                this.mSoundManager.onPause();
            }
            this.mRunning = false;
        }
    }

    /**
     * The {@link org.andengine.engine.Engine.EngineLock} can be used to {@link org.andengine.engine.Engine.EngineLock#lock()}/{@link org.andengine.engine.Engine.EngineLock#unlock()} on, to ensure the code in between runs mutually exclusive to the {@link org.andengine.engine.Engine.UpdateThread} and the GL{@link Thread}.
     * When the caller already is on the {@link org.andengine.engine.Engine.UpdateThread} or the GL-{@link Thread}, that code is executed immediately.
     *
     * @return the {@link org.andengine.engine.Engine.EngineLock} the {@link org.andengine.engine.Engine} locks on to ensure mutually exclusivity to the {@link org.andengine.engine.Engine.UpdateThread} and the GL{@link Thread}.
     */
    public EngineLock getEngineLock() {
        return this.mEngineLock;
    }

    public Scene getScene() {
        return this.mScene;
    }

    public void setScene(final Scene pScene) {
        this.mScene = pScene;
    }

    public EngineOptions getEngineOptions() {
        return this.mEngineOptions;
    }

    public Camera getCamera() {
        return this.mCamera;
    }

    public float getSecondsElapsedTotal() {
        return this.mSecondsElapsedTotal;
    }

    public void setSurfaceSize(final int pSurfaceWidth, final int pSurfaceHeight) {
        this.mSurfaceWidth = pSurfaceWidth;
        this.mSurfaceHeight = pSurfaceHeight;
        this.onUpdateCameraSurface();
    }

    protected void onUpdateCameraSurface() {
        this.mCamera.setSurfaceSize(0, 0, this.mSurfaceWidth, this.mSurfaceHeight);
    }

    public int getSurfaceWidth() {
        return this.mSurfaceWidth;
    }

    public int getSurfaceHeight() {
        return this.mSurfaceHeight;
    }

    public ITouchController getTouchController() {
        return this.mTouchController;
    }

    public void setTouchController(final ITouchController pTouchController) {
        this.mTouchController = pTouchController;
        this.mTouchController.setTouchEventCallback(this);
    }

    public AccelerationData getAccelerationData() {
        return this.mAccelerationData;
    }

    public OrientationData getOrientationData() {
        return this.mOrientationData;
    }

    public VertexBufferObjectManager getVertexBufferObjectManager() {
        return this.mVertexBufferObjectManager;
    }

    public TextureManager getTextureManager() {
        return this.mTextureManager;
    }

    public FontManager getFontManager() {
        return this.mFontManager;
    }

    public ShaderProgramManager getShaderProgramManager() {
        return this.mShaderProgramManager;
    }

    public SoundManager getSoundManager() throws IllegalStateException {
        if (this.mSoundManager != null) {
            return this.mSoundManager;
        } else {
            throw new IllegalStateException("To enable the SoundManager, check the EngineOptions!");
        }
    }

    public MusicManager getMusicManager() throws IllegalStateException {
        if (this.mMusicManager != null) {
            return this.mMusicManager;
        } else {
            throw new IllegalStateException("To enable the MusicManager, check the EngineOptions!");
        }
    }

    public void registerUpdateHandler(final IUpdateHandler pUpdateHandler) {
        this.mUpdateHandlers.add(pUpdateHandler);
    }

    public void unregisterUpdateHandler(final IUpdateHandler pUpdateHandler) {
        this.mUpdateHandlers.remove(pUpdateHandler);
    }

    public void clearUpdateHandlers() {
        this.mUpdateHandlers.clear();
    }

    public void registerDrawHandler(final IDrawHandler pDrawHandler) {
        this.mDrawHandlers.add(pDrawHandler);
    }

    public void unregisterDrawHandler(final IDrawHandler pDrawHandler) {
        this.mDrawHandlers.remove(pDrawHandler);
    }

    public void clearDrawHandlers() {
        this.mDrawHandlers.clear();
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    public void onAccuracyChanged(final Sensor pSensor, final int pAccuracy) {
        if (this.mRunning) {
            final int sensorType = pSensor.getType();
            switch (sensorType) {
                case Sensor.TYPE_ACCELEROMETER:
                    if (this.mAccelerationData != null) {
                        this.mAccelerationData.setAccuracy(pAccuracy);
                        this.mAccelerationListener.onAccelerationAccuracyChanged(this.mAccelerationData);
                    } else if (this.mOrientationData != null) {
                        this.mOrientationData.setAccelerationAccuracy(pAccuracy);
                        this.mOrientationListener.onOrientationAccuracyChanged(this.mOrientationData);
                    }
                    break;
                case Sensor.TYPE_MAGNETIC_FIELD:
                    this.mOrientationData.setMagneticFieldAccuracy(pAccuracy);
                    this.mOrientationListener.onOrientationAccuracyChanged(this.mOrientationData);
                    break;
                default:
                    throw new IllegalArgumentException("Unexpected " + Sensor.class.getSimpleName() + " of Type: '" + sensorType + "'.");
            }
        }
    }

    @Override
    public void onSensorChanged(final SensorEvent pEvent) {
        if (this.mRunning) {
            final Sensor sensor = pEvent.sensor;
            final int sensorType = sensor.getType();
            switch (sensorType) {
                case Sensor.TYPE_ACCELEROMETER:
                    if (this.mAccelerationData != null) {
                        this.mAccelerationData.setDisplayRotation(this.getDisplayOrientation());
                        this.mAccelerationData.setValues(pEvent.values);
                        this.mAccelerationListener.onAccelerationChanged(this.mAccelerationData);
                    } else if (this.mOrientationData != null) {
                        this.mOrientationData.setDisplayRotation(this.getDisplayOrientation());
                        this.mOrientationData.setAccelerationValues(pEvent.values);
                        this.mOrientationListener.onOrientationChanged(this.mOrientationData);
                    }
                    break;
                case Sensor.TYPE_MAGNETIC_FIELD:
                    this.mOrientationData.setDisplayRotation(this.getDisplayOrientation());
                    this.mOrientationData.setMagneticFieldValues(pEvent.values);
                    this.mOrientationListener.onOrientationChanged(this.mOrientationData);
                    break;
                default:
                    throw new IllegalArgumentException("Unexpected " + Sensor.class.getSimpleName() + " of Type: '" + sensorType + "'.");
            }
        }
    }

    @Override
    public void onLocationChanged(final Location pLocation) {
        if (this.mLocation == null) {
            this.mLocation = pLocation;
        } else {
            if (pLocation == null) {
                this.mLocationListener.onLocationLost();
            } else {
                this.mLocation = pLocation;
                this.mLocationListener.onLocationChanged(pLocation);
            }
        }
    }

    @Override
    public void onProviderDisabled(final String pProvider) {
        this.mLocationListener.onLocationProviderDisabled();
    }

    @Override
    public void onProviderEnabled(final String pProvider) {
        this.mLocationListener.onLocationProviderEnabled();
    }

    @Override
    public void onStatusChanged(final String pProvider, final int pStatus, final Bundle pExtras) {
        switch (pStatus) {
            case LocationProvider.AVAILABLE:
                this.mLocationListener.onLocationProviderStatusChanged(LocationProviderStatus.AVAILABLE, pExtras);
                break;
            case LocationProvider.OUT_OF_SERVICE:
                this.mLocationListener.onLocationProviderStatusChanged(LocationProviderStatus.OUT_OF_SERVICE, pExtras);
                break;
            case LocationProvider.TEMPORARILY_UNAVAILABLE:
                this.mLocationListener.onLocationProviderStatusChanged(LocationProviderStatus.TEMPORARILY_UNAVAILABLE, pExtras);
                break;
            default:
                throw new IllegalArgumentException("Unexpected " + LocationProvider.class.getSimpleName() + ": '" + pStatus + "'.");
        }
    }

    @Override
    public boolean onTouch(final View pView, final MotionEvent pSurfaceMotionEvent) {
        if (this.mRunning) {
            this.mTouchController.onHandleMotionEvent(pSurfaceMotionEvent);
            try {
                /* Because a human cannot interact 1000x per second, we pause the UI-Thread for a little. */
                Thread.sleep(this.mEngineOptions.getTouchOptions().getTouchEventIntervalMilliseconds());
            } catch (final InterruptedException e) {
                Debug.e(e);
            }
            return true;
        } else {
            return false;
        }
    }

    @Override
    public boolean onTouchEvent(final TouchEvent pSurfaceTouchEvent) {
        /* Let the engine determine which scene and camera this event should be handled by. */
        final Scene scene = this.getSceneFromSurfaceTouchEvent(pSurfaceTouchEvent);
        final Camera camera = this.getCameraFromSurfaceTouchEvent(pSurfaceTouchEvent);

        this.convertSurfaceTouchEventToSceneTouchEvent(camera, pSurfaceTouchEvent);

        if (this.onTouchHUD(camera, pSurfaceTouchEvent)) {
            return true;
        } else {
			/* If HUD didn't handle it, Scene may handle it. */
            return this.onTouchScene(scene, pSurfaceTouchEvent);
        }
    }

    protected boolean onTouchHUD(final Camera pCamera, final TouchEvent pSceneTouchEvent) {
        if (pCamera.hasHUD()) {
            return pCamera.getHUD().onSceneTouchEvent(pSceneTouchEvent);
        } else {
            return false;
        }
    }

    protected boolean onTouchScene(final Scene pScene, final TouchEvent pSceneTouchEvent) {
        if (pScene != null) {
            return pScene.onSceneTouchEvent(pSceneTouchEvent);
        } else {
            return false;
        }
    }

    // ===========================================================
    // Methods
    // ===========================================================

    public void runOnUpdateThread(final Runnable pRunnable) {
        this.runOnUpdateThread(pRunnable, true);
    }

    /**
     * This method is useful when you want to execute code on the {@link org.andengine.engine.Engine.UpdateThread}, even though the Engine is paused.
     *
     * @param pRunnable              the {@link Runnable} to be run on the {@link org.andengine.engine.Engine.UpdateThread}.
     * @param pOnlyWhenEngineRunning if <code>true</code>, the execution of the {@link Runnable} will be delayed until the next time {@link #onUpdateUpdateHandlers(float)} is picked up, which is when {@link org.andengine.engine.Engine#isRunning()} is <code>true</code>.
     *                               if <code>false</code>, the execution of the {@link Runnable} will happen as soon as possible on the {@link org.andengine.engine.Engine.UpdateThread}, no matter what {@link #isRunning()} is.
     */
    public void runOnUpdateThread(final Runnable pRunnable, final boolean pOnlyWhenEngineRunning) {
        if (pOnlyWhenEngineRunning) {
            this.mUpdateThreadRunnableHandler.postRunnable(pRunnable);
        } else {
            this.mUpdateThread.postRunnable(pRunnable);
        }
    }

    /**
     * @param pRunnable the {@link Runnable} to run mutually exclusive to the {@link org.andengine.engine.Engine.UpdateThread} and the GL-{@link Thread}.
     *                  When the caller already is on the {@link org.andengine.engine.Engine.UpdateThread} or the GL-{@link Thread}, the {@link Runnable} is executed immediately.
     * @see {@link #getEngineLock()} to manually {@link org.andengine.engine.Engine.EngineLock#lock()}/{@link org.andengine.engine.Engine.EngineLock#unlock()} on, while avoiding creating a {@link Runnable}.
     */
    public void runSafely(final Runnable pRunnable) {
        this.mEngineLock.lock();
        try {
            pRunnable.run();
        } finally {
            this.mEngineLock.unlock();
        }
    }

    public void onDestroy() {
        this.mEngineLock.lock();
        try {
            this.mDestroyed = true;
            this.mEngineLock.notifyCanUpdate();
        } finally {
            this.mEngineLock.unlock();
        }
        try {
            this.mUpdateThread.join();
        } catch (final InterruptedException e) {
            Debug.e("Could not join UpdateThread.", e);
            Debug.w("Trying to manually interrupt UpdateThread.");
            this.mUpdateThread.interrupt();
        }

        this.releaseDefaultDisplay();

        this.mVertexBufferObjectManager.onDestroy();
        this.mTextureManager.onDestroy();
        this.mFontManager.onDestroy();
        this.mShaderProgramManager.onDestroy();
    }

    public void onReloadResources() {
        this.mVertexBufferObjectManager.onReload();
        this.mTextureManager.onReload();
        this.mFontManager.onReload();
        this.mShaderProgramManager.onReload();
    }

    protected Camera getCameraFromSurfaceTouchEvent(final TouchEvent pTouchEvent) {
        return this.getCamera();
    }

    protected Scene getSceneFromSurfaceTouchEvent(final TouchEvent pTouchEvent) {
        return this.mScene;
    }

    protected void convertSurfaceTouchEventToSceneTouchEvent(final Camera pCamera, final TouchEvent pSurfaceTouchEvent) {
        pCamera.convertSurfaceTouchEventToSceneTouchEvent(pSurfaceTouchEvent, this.mSurfaceWidth, this.mSurfaceHeight);
    }

    protected void convertSceneTouchEventToSurfaceTouchEvent(final Camera pCamera, final TouchEvent pSurfaceTouchEvent) {
        pCamera.convertSceneTouchEventToSurfaceTouchEvent(pSurfaceTouchEvent, this.mSurfaceWidth, this.mSurfaceHeight);
    }

    void onTickUpdate() throws InterruptedException {
        if (this.mRunning) {
            final long secondsElapsed = this.getNanosecondsElapsed();

            this.mEngineLock.lock();
            try {
                this.throwOnDestroyed();

                this.onUpdate(secondsElapsed);

                this.throwOnDestroyed();

                this.mEngineLock.notifyCanDraw();
                this.mEngineLock.waitUntilCanUpdate();
            } finally {
                this.mEngineLock.unlock();
            }
        } else {
            this.mEngineLock.lock();
            try {
                this.throwOnDestroyed();

                this.mEngineLock.notifyCanDraw();
                this.mEngineLock.waitUntilCanUpdate();
            } finally {
                this.mEngineLock.unlock();
            }

            Thread.sleep(16);
        }
    }

    private void throwOnDestroyed() throws EngineDestroyedException {
        if (this.mDestroyed) {
            throw new EngineDestroyedException();
        }
    }

    public void onUpdate(final long pNanosecondsElapsed) throws InterruptedException {
        final float pSecondsElapsed = pNanosecondsElapsed * TimeConstants.SECONDS_PER_NANOSECOND;

        this.mSecondsElapsedTotal += pSecondsElapsed;
        this.mLastTick += pNanosecondsElapsed;

        this.mTouchController.onUpdate(pSecondsElapsed);
        this.onUpdateUpdateHandlers(pSecondsElapsed);
        this.onUpdateScene(pSecondsElapsed);
    }

    protected void onUpdateScene(final float pSecondsElapsed) {
        if (this.mScene != null) {
            this.mScene.onUpdate(pSecondsElapsed);
        }
        this.getCamera().onUpdate(pSecondsElapsed);
    }

    protected void onUpdateUpdateHandlers(final float pSecondsElapsed) {
        this.mUpdateThreadRunnableHandler.onUpdate(pSecondsElapsed);
        this.mUpdateHandlers.onUpdate(pSecondsElapsed);
    }

    protected void onUpdateDrawHandlers(final GLState pGLState, final Camera pCamera) {
        this.mDrawHandlers.onDraw(pGLState, pCamera);
    }

    public void onDrawFrame(final GLState pGLState) throws InterruptedException {
        final EngineLock engineLock = this.mEngineLock;

        engineLock.lock();
        try {
            engineLock.waitUntilCanDraw();

            this.mVertexBufferObjectManager.updateVertexBufferObjects(pGLState);
            this.mTextureManager.updateTextures(pGLState);
            this.mFontManager.updateFonts(pGLState);

            this.onUpdateDrawHandlers(pGLState, this.mCamera);
            this.onDrawScene(pGLState, this.mCamera);

            engineLock.notifyCanUpdate();
        } finally {
            engineLock.unlock();
        }
    }

    protected void onDrawScene(final GLState pGLState, final Camera pCamera) {
        if (this.mScene != null) {
            this.mScene.onDraw(pGLState, pCamera);
        }

        pCamera.onDrawHUD(pGLState);
    }

    private long getNanosecondsElapsed() {
        final long now = System.nanoTime();

        return now - this.mLastTick;
    }

    public boolean enableVibrator(final Context pContext) {
        this.mVibrator = (Vibrator) pContext.getSystemService(Context.VIBRATOR_SERVICE);
        return this.mVibrator != null;
    }

    public void vibrate(final long pMilliseconds) throws IllegalStateException {
        if (this.mVibrator != null) {
            this.mVibrator.vibrate(pMilliseconds);
        } else {
            throw new IllegalStateException("You need to enable the Vibrator before you can use it!");
        }
    }

    public void vibrate(final long[] pPattern, final int pRepeat) throws IllegalStateException {
        if (this.mVibrator != null) {
            this.mVibrator.vibrate(pPattern, pRepeat);
        } else {
            throw new IllegalStateException("You need to enable the Vibrator before you can use it!");
        }
    }

    public void enableLocationSensor(final Context pContext, final ILocationListener pLocationListener, final LocationSensorOptions pLocationSensorOptions) {
        this.mLocationListener = pLocationListener;

        final LocationManager locationManager = (LocationManager) pContext.getSystemService(Context.LOCATION_SERVICE);
        final String locationProvider = locationManager.getBestProvider(pLocationSensorOptions, pLocationSensorOptions.isEnabledOnly());
        // TODO locationProvider can be null, in that case return false. Successful case should return true.
        locationManager.requestLocationUpdates(locationProvider, pLocationSensorOptions.getMinimumTriggerTime(), pLocationSensorOptions.getMinimumTriggerDistance(), this);

        this.onLocationChanged(locationManager.getLastKnownLocation(locationProvider));
    }

    public void disableLocationSensor(final Context pContext) {
        final LocationManager locationManager = (LocationManager) pContext.getSystemService(Context.LOCATION_SERVICE);
        locationManager.removeUpdates(this);
    }

    /**
     * @see {@link #enableAccelerationSensor(android.content.Context, org.andengine.input.sensor.acceleration.IAccelerationListener, org.andengine.input.sensor.acceleration.AccelerationSensorOptions)}
     */
    public boolean enableAccelerationSensor(final Context pContext, final IAccelerationListener pAccelerationListener) {
        return this.enableAccelerationSensor(pContext, pAccelerationListener, new AccelerationSensorOptions(Engine.SENSORDELAY_DEFAULT));
    }

    /**
     * @return <code>true</code> when the sensor was successfully enabled, <code>false</code> otherwise.
     */
    public boolean enableAccelerationSensor(final Context pContext, final IAccelerationListener pAccelerationListener, final AccelerationSensorOptions pAccelerationSensorOptions) {
        final SensorManager sensorManager = (SensorManager) pContext.getSystemService(Context.SENSOR_SERVICE);
        if (this.isSensorSupported(sensorManager, Sensor.TYPE_ACCELEROMETER)) {
            this.mAccelerationListener = pAccelerationListener;

            this.initDefaultDisplay(pContext);

            if (this.mAccelerationData == null) {
                this.mAccelerationData = new AccelerationData();
            }

            this.registerSelfAsSensorListener(sensorManager, Sensor.TYPE_ACCELEROMETER, pAccelerationSensorOptions.getSensorDelay());

            return true;
        } else {
            return false;
        }
    }

    /**
     * @return <code>true</code> when the sensor was successfully disabled, <code>false</code> otherwise.
     */
    public boolean disableAccelerationSensor(final Context pContext) {
        final SensorManager sensorManager = (SensorManager) pContext.getSystemService(Context.SENSOR_SERVICE);
        if (this.isSensorSupported(sensorManager, Sensor.TYPE_ACCELEROMETER)) {
            this.unregisterSelfAsSensorListener(sensorManager, Sensor.TYPE_ACCELEROMETER);
            return true;
        } else {
            return false;
        }
    }

    /**
     * @see {@link #enableOrientationSensor(android.content.Context, org.andengine.input.sensor.orientation.IOrientationListener, org.andengine.input.sensor.orientation.OrientationSensorOptions)}
     */
    public boolean enableOrientationSensor(final Context pContext, final IOrientationListener pOrientationListener) {
        return this.enableOrientationSensor(pContext, pOrientationListener, new OrientationSensorOptions(Engine.SENSORDELAY_DEFAULT));
    }

    /**
     * @return <code>true</code> when the sensor was successfully enabled, <code>false</code> otherwise.
     */
    public boolean enableOrientationSensor(final Context pContext, final IOrientationListener pOrientationListener, final OrientationSensorOptions pOrientationSensorOptions) {
        final SensorManager sensorManager = (SensorManager) pContext.getSystemService(Context.SENSOR_SERVICE);
        if (this.isSensorSupported(sensorManager, Sensor.TYPE_ACCELEROMETER) && this.isSensorSupported(sensorManager, Sensor.TYPE_MAGNETIC_FIELD)) {
            this.mOrientationListener = pOrientationListener;

            this.initDefaultDisplay(pContext);

            if (this.mOrientationData == null) {
                this.mOrientationData = new OrientationData();
            }

            this.registerSelfAsSensorListener(sensorManager, Sensor.TYPE_ACCELEROMETER, pOrientationSensorOptions.getSensorDelay());
            this.registerSelfAsSensorListener(sensorManager, Sensor.TYPE_MAGNETIC_FIELD, pOrientationSensorOptions.getSensorDelay());

            return true;
        } else {
            return false;
        }
    }


    /**
     * @return <code>true</code> when the sensor was successfully disabled, <code>false</code> otherwise.
     */
    public boolean disableOrientationSensor(final Context pContext) {
        final SensorManager sensorManager = (SensorManager) pContext.getSystemService(Context.SENSOR_SERVICE);
        if (this.isSensorSupported(sensorManager, Sensor.TYPE_ACCELEROMETER) && this.isSensorSupported(sensorManager, Sensor.TYPE_MAGNETIC_FIELD)) {
            this.unregisterSelfAsSensorListener(sensorManager, Sensor.TYPE_ACCELEROMETER);
            this.unregisterSelfAsSensorListener(sensorManager, Sensor.TYPE_MAGNETIC_FIELD);
            return true;
        } else {
            return false;
        }
    }

    private boolean isSensorSupported(final SensorManager pSensorManager, final int pType) {
        return pSensorManager.getSensorList(pType).size() > 0;
    }

    private void registerSelfAsSensorListener(final SensorManager pSensorManager, final int pType, final SensorDelay pSensorDelay) {
        final Sensor sensor = pSensorManager.getSensorList(pType).get(0);
        pSensorManager.registerListener(this, sensor, pSensorDelay.getDelay());
    }

    private void unregisterSelfAsSensorListener(final SensorManager pSensorManager, final int pType) {
        final Sensor sensor = pSensorManager.getSensorList(pType).get(0);
        pSensorManager.unregisterListener(this, sensor);
    }

    private void initDefaultDisplay(final Context pContext) {
        if (this.mDefaultDisplay == null) {
            final WindowManager windowManager = (WindowManager) pContext.getSystemService(Context.WINDOW_SERVICE);
            this.mDefaultDisplay = windowManager.getDefaultDisplay();
        }
    }

    private void releaseDefaultDisplay() {
        this.mDefaultDisplay = null;
    }

    private int getDisplayOrientation() {
        if (this.mDefaultDisplay == null) {
            throw new IllegalStateException();
        } else {
            return this.mDefaultDisplay.getOrientation();
        }
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public static class UpdateThread extends Thread {
        // ===========================================================
        // Constants
        // ===========================================================

        // ===========================================================
        // Fields
        // ===========================================================

        private final RunnableHandler mRunnableHandler = new RunnableHandler();
        private Engine mEngine;

        // ===========================================================
        // Constructors
        // ===========================================================

        public UpdateThread() {
            super(UpdateThread.class.getSimpleName());
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        public void setEngine(final Engine pEngine) {
            this.mEngine = pEngine;
        }

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        @Override
        public void run() {
            android.os.Process.setThreadPriority(this.mEngine.getEngineOptions().getUpdateThreadPriority());
            try {
                while (true) {
                    this.mRunnableHandler.onUpdate(0);
                    this.mEngine.onTickUpdate();
                }
            } catch (final InterruptedException e) {
                if (BuildConfig.DEBUG) {
                    Debug.d(this.getClass().getSimpleName() + " interrupted. Don't worry - this " + e.getClass().getSimpleName() + " is most likely expected!", e);
                }
                this.interrupt();
            }
        }

        // ===========================================================
        // Methods
        // ===========================================================

        public void postRunnable(final Runnable pRunnable) {
            this.mRunnableHandler.postRunnable(pRunnable);
        }

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }

    public static class EngineLock extends ReentrantLock {
        // ===========================================================
        // Constants
        // ===========================================================

        private static final long serialVersionUID = 671220941302523934L;

        // ===========================================================
        // Fields
        // ===========================================================

        final Condition mDrawingCondition = this.newCondition();
        final AtomicBoolean mDrawing = new AtomicBoolean(false);

        // ===========================================================
        // Constructors
        // ===========================================================

        public EngineLock(final boolean pFair) {
            super(pFair);
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        void notifyCanDraw() {
            this.mDrawing.set(true);
            this.mDrawingCondition.signalAll();
        }

        void notifyCanUpdate() {
            this.mDrawing.set(false);
            this.mDrawingCondition.signalAll();
        }

        void waitUntilCanDraw() throws InterruptedException {
            while (!this.mDrawing.get()) {
                this.mDrawingCondition.await();
            }
        }

        void waitUntilCanUpdate() throws InterruptedException {
            while (this.mDrawing.get()) {
                this.mDrawingCondition.await();
            }
        }

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }

    public class EngineDestroyedException extends InterruptedException {
        // ===========================================================
        // Constants
        // ===========================================================

        private static final long serialVersionUID = -4691263961728972560L;

        // ===========================================================
        // Fields
        // ===========================================================

        // ===========================================================
        // Constructors
        // ===========================================================

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }
}
